
/* Lightweight ultrafast and powerful EventEmitter that built on the native JavaScript emitter.
 *
 * @param this (Object) An optional parameter that allows to extend any object
 * @returns Object (EventBus|this)
 */
export default function EventBus (that) {
  var that = that || this,
      id = (new Date).getTime() + "" + Math.random(),
      node = document.createElement("EventBus"),
      recent = {};

  that.on = on;
  that.off = off;
  that.emit = emit;

  /*
   * The method registers the specified listener on single or multiple space or comma separated events.
   * Multiple calls passing the same combination of event and listener will not result in the listener being added.
   * Any event that passed as argument can contain any combination of pseudo-prefixes `:ready` and/or `:once`.
   * `:ready` - Indicates that the listener should be called even if the event has been emitted before the listener was added.
   * `:once`  - Indicates that the listener should be unsubscribed at once event will emitted.
   *
   * @param event (String) Single or multiple space or comma separated events that can contain of pseudo-prefixes `:ready` and/or `:once`
   * @param listener (Function) The function that receives a notification when an event of the specified type occurs
   * @returns (EventBus|this)
   */
  function on (event, handler) {
    var event = unify(event);
    if (typeof event == "string" && typeof handler == "function") {
      var event = event.replace(/(([\w._\-~\[\]$#&^]+)(:ready|:once)(:ready|:once)?)/ig, function ($0, $1, $e, $3, $4) {
        var ready = /:ready/i.test($3 + $4),
            once = /:once/i.test($3 + $4),
            last = recent[$e],
            done;
        if (!once) listener("on", $e, node, handler, false);
        if (ready && last) {
          if (once) done = true;
          setTimeout(function(){handler(last)}, 0);
        }
        if (once && !done) {
          var fn = function (e) {
            listener("off", $e, node, fn, false);
            handler(e);
          };
          listener("on", $e, node, fn, false);
        }
        return "";
      });
      listener("on", cleanPseudo(event), node, handler, false);
    }
    return that;
  }

  /*
   * The method removes single or multiple space or comma separated events listener previously registered with EventBus.on().
   *
   * @param event (String) Single or multiple space or comma separated events
   * @param listener (Function) The function that receives a notification when an event of the specified type occurs
   * @returns (EventBus|this)
   */
  function off (event, handler) {
    var event = unify(cleanPseudo(event));
    if (!event) return that;
    listener("off", event, node, handler, false);
    return that;
  }

  /*
   * Asynchronously calls each of the listeners registered for the event named eventName, passing the supplied data to each.
   *
   * @param event (String) Single event name
   * @param data (Object) An optional data object
   * @param target (Any) Event.target
   * @param preventDefault (Function) Event.preventDefault handler
   * @param stopPropagation (Function) Event.stopPropagation handler
   * @returns (EventBus|this)
   */
  function emit (event, data, target, preventDefault, stopPropagation) {
    var event = unify(cleanPseudo(event));
    if (!event) return that;
    recent[event] = dispatch(node, event, data, target, preventDefault, stopPropagation);
    return that;
  }

  function unify (event) {
    if (event && typeof event == "string") return event.toLowerCase();
  }

  function cleanPseudo (events) {
    return typeof events == "string" ? events.replace(/:ready|:once/ig, '') : events;
  }

  function eventHandlerStatus (handler, event, status) {
    if (!handler[id]) handler[id] = {};
    if (typeof status == "boolean") handler[id][event] = status;
    return !!handler[id][event];
  }

  function listener (type, event, node, fn, sign) {
    if (typeof type != "string" || typeof event != "string" || !event.length || !node || typeof fn != "function") return;
    if (typeof node == "object" && typeof node.length == "number" && !node.nodeName && node != window) {
      for (var i=0; i<node.length; i++) listener(type, event, node[i], fn, sign);
      return;
    }
    var add = !!(type == "on");
    var method = add ? "addEventListener" : "removeEventListener";
    var e = event.replace(/^\s+|\s+$/img, '').split(/[ ,]+/);
    if (e.length == 1) {
      if (e[0].length) {
        var status = eventHandlerStatus(fn, e[0]);
        if ((add && !status) || !add) {
          node[method](e[0], fn, !!sign);
          eventHandlerStatus(fn, e[0], add);
        }
      }
      return;
    }
    for(var i=0; i<e.length; i++) listener(type, e[i], node, fn, sign);
  }

  function dispatch (node, event, data, target, prevent, stop) {
    var e = document.createEvent("HTMLEvents");
    var _prevent = e.preventDefault;
    var _stop = e.stopImmediatePropagation;
    function stopPropagation () {
      e.propagationStopped = true;
      _stop.call(e);
      if (typeof stop == "function") stop(e);
    }
    function preventDefault () {
      _prevent.call(e);
      if (typeof prevent == "function") prevent(e);
      return false;
    }
    e.data = data;
    if (target) {
      var descriptor = {value: target};
      Object.defineProperties(e, {
        target: descriptor,
        currentTarget: descriptor,
        srcElement: descriptor
      });
    }
    e.preventDefault = preventDefault;
    e.stopPropagation = stopPropagation;
    e.stopImmediatePropagation = stopPropagation;
    e.initEvent(event, true, true);
    node.dispatchEvent(e);
    return e;
  }

}